package com.such.kit.remote;

import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.Proxy.Type;
import java.net.URL;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Map;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.apache.http.client.config.AuthSchemes;
import org.apache.http.client.config.CookieSpecs;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.config.RequestConfig.Builder;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.entity.mime.HttpMultipartMode;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.entity.mime.content.ContentBody;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.util.CharsetUtils;
import org.apache.http.util.EntityUtils;
import com.such.kit.Callable;
import com.such.kit.Logger;
import com.such.kit.easyparam.ParamList;
import com.such.kit.stream.StreamCommons;
import com.such.kit.string.StringCommons;
import com.such.kit.validate.ValidateCommons;

/**
 * @author SUCH
 * <pre>
 * URL 请求模拟工具
 * </pre>
 */
public class URLConnectionCommons {

	private static final String http = "http";
	private static final String https = "https";

	/**
	 * <pre>
	 * 基于 JDK {@link HttpURLConnection} 的 web 请求模拟
	 * </pre>
	 * @param address 请求地址
	 * @param method 请求方法 [参考 {@link HttpURLConnection#setRequestMethod(String)}]
	 * @param report 报文体
	 * @param charset 编码
	 * @param connectionSet 链接参数设置 [可空]
	 * @param proxyScheme 代理协议 [可空]
	 * @param proxyAddress 代理地址 [可空]
	 * @param proxyPort 代理端口
	 * @return 请求反馈
	 * @throws Exception
	 */
	public static String request(String address, String method, String report, String charset, Callable<HttpURLConnection, Void> connectionSet,
			Type proxyScheme, String proxyAddress, int proxyPort) throws Exception {
		Logger.debug(URLConnectionCommons.class, StringCommons.merger("请求报文：", StringCommons.NEW_LINE, report));
		// 1. 创建服务地址
		URL url = new URL(address);
		// 2. 打开连接
		HttpURLConnection connection = null;
		if (ValidateCommons.isNotEmpty(proxyScheme) && ValidateCommons.isNotEmpty(proxyAddress) && proxyPort >= 0) {
			Logger.debug(URLConnectionCommons.class, StringCommons.merger("使用代理：[", proxyScheme, "] ", proxyAddress, ":", proxyPort));
			Proxy proxy = new Proxy(proxyScheme, new InetSocketAddress(proxyAddress, proxyPort));
			connection = (HttpURLConnection) url.openConnection(proxy);
		} else {
			connection = (HttpURLConnection) url.openConnection();
		}
		// 3. 设置参数
		connection.setRequestMethod(method);
		connection.setDoInput(true); // 允许向 InputStream 中写入数据
		connection.setDoOutput(true); // 允许从 OutputStream 中读取数据
		if (address.toUpperCase().startsWith("HTTPS")) {
			((HttpsURLConnection) connection).setSSLSocketFactory(getSSLContext().getSocketFactory());
			((HttpsURLConnection) connection).setHostnameVerifier(new HostnameVerifier() {
				@Override
				public boolean verify(String hostname, SSLSession session) {
					return true;
				}
			});
		}
		if (ValidateCommons.isNotEmpty(connectionSet)) {
			connectionSet.call(connection);
		}
		String response = "";
		OutputStream out = null;
		try {
			// 4. 发送请求
			if (ValidateCommons.isNotEmpty(report)) {
				out = connection.getOutputStream();
				out.write(report.getBytes(charset));
			}
			// 5. 接收响应
			int responseCode = connection.getResponseCode();
			if (200 == responseCode) {
				// 200 服务端响应成功
				InputStream in = connection.getInputStream();
				response = StreamCommons.textReader(new InputStreamReader(in, charset));
			} else {
				Logger.error(URLConnectionCommons.class, StringCommons.merger(
						StringCommons.NEW_LINE,
						"请求失败，响应码 [", responseCode, "]",
						StringCommons.NEW_LINE,
						StreamCommons.textReader(new InputStreamReader(connection.getInputStream(), charset)),
						StringCommons.NEW_LINE
				));
			}
		} finally {
			StreamCommons.closePure(out);
		}
		return response;
	}

	/**
	 * <pre>
	 * 基于 JDK {@link HttpURLConnection} 的 web 请求模拟
	 * </pre>
	 * @param address 请求地址
	 * @param method 请求方法 [参考 {@link HttpURLConnection#setRequestMethod(String)}]
	 * @param report 报文体
	 * @param charset 编码
	 * @param connectionSet 链接参数设置 [可空]
	 * @return 请求反馈
	 * @throws Exception
	 */
	public static String request(String address, String method, String report, String charset, Callable<HttpURLConnection, Void> connectionSet) throws Exception {
		return request(address, method, report, charset, connectionSet, null, null, -1);
	}

	/**
	 * <pre>
	 * 基于 JDK {@link HttpURLConnection} 的 web 请求模拟
	 * </pre>
	 * @param address 请求地址
	 * @param method 请求方法 [参考 {@link HttpURLConnection#setRequestMethod(String)}]
	 * @param report 报文体
	 * @param charset 编码
	 * @return 请求反馈
	 * @throws Exception
	 */
	public static String request(String address, String method, String report, String charset) throws Exception {
		return request(address, method, report, charset, null, null, null, -1);
	}

	/**
	 * <pre>
	 * 基于 apache {@link HttpClients} 的 POST 表单请求模拟
	 * </pre>
	 * @param address 请求地址
	 * @param parts 表单组件集
	 * @param charset 编码
	 * @param headerSet 请求头设置 [可空]
	 * @param proxyScheme 代理协议 [可空]
	 * @param proxyAddress 代理地址 [可空]
	 * @param proxyPort 代理端口
	 * @return 请求反馈
	 * @throws Exception
	 */
	public static String post(String address, Map<String, ContentBody> parts, String charset, Callable<HttpPost, Void> headerSet,
			String proxyScheme, String proxyAddress, int proxyPort) throws Exception {
		HttpEntity entity = null;
		if (ValidateCommons.isNotEmpty(parts)) {
			MultipartEntityBuilder builder = MultipartEntityBuilder.create(); // 实例化实体构造器
			builder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE); // 设置浏览器兼容模式
			builder.setCharset(CharsetUtils.get(charset));
			for (String key : parts.keySet()) {
				builder.addPart(key, parts.get(key));
			}
			entity = builder.build(); // 设置请求的编码格式，并构造实体
		}
		return post(address, entity, charset, headerSet, proxyScheme, proxyAddress, proxyPort);
	}

	/**
	 * <pre>
	 * 基于 apache {@link HttpClients} 的 POST 表单请求模拟
	 * </pre>
	 * @param address 请求地址
	 * @param parts 表单组件集
	 * @param charset 编码
	 * @param headerSet 请求头设置 [可空]
	 * @return 请求反馈
	 * @throws Exception
	 */
	public static String post(String address, Map<String, ContentBody> parts, String charset, Callable<HttpPost, Void> headerSet) throws Exception {
		return post(address, parts, charset, headerSet, null, null, -1);
	}

	/**
	 * <pre>
	 * 基于 apache {@link HttpClients} 的 POST 表单请求模拟
	 * </pre>
	 * @param address 请求地址
	 * @param parts 表单组件集
	 * @param charset 编码
	 * @return 请求反馈
	 * @throws Exception
	 */
	public static String post(String address, Map<String, ContentBody> parts, String charset) throws Exception {
		return post(address, parts, charset, null, null, null, -1);
	}

	/**
	 * <pre>
	 * 基于 apache {@link HttpClients} 的 POST 异步（json）请求模拟
	 * </pre>
	 * @param address 请求地址
	 * @param json json
	 * @param charset 编码
	 * @param headerSet 请求头设置 [可空]
	 * @param proxyScheme 代理协议 [可空]
	 * @param proxyAddress 代理地址 [可空]
	 * @param proxyPort 代理端口
	 * @return 请求反馈
	 * @throws Exception
	 */
	public static String post(String address, String json, String charset, final Callable<HttpPost, Void> headerSet,
			String proxyScheme, String proxyAddress, int proxyPort) throws Exception {
		HttpEntity entity = null;
		if (ValidateCommons.isNotEmpty(json)) {
			entity = new StringEntity(json, charset);
		}
		return post(address, entity, charset, new Callable<HttpPost, Void>() {
			@Override
			public Void call(HttpPost post) throws Exception {
				if (ValidateCommons.isNotEmpty(headerSet)) {
					headerSet.call(post);
				}
				post.setHeader("Content-Type", ContentType.create("application/json", charset).toString());
				return null;
			}
		}, proxyScheme, proxyAddress, proxyPort);
	}

	/**
	 * <pre>
	 * 基于 apache {@link HttpClients} 的 POST 异步（json）请求模拟
	 * </pre>
	 * @param address 请求地址
	 * @param json json
	 * @param charset 编码
	 * @param headerSet 请求头设置
	 * @return 请求反馈
	 * @throws Exception
	 */
	public static String post(String address, String json, String charset, Callable<HttpPost, Void> headerSet) throws Exception {
		return post(address, json, charset, headerSet, null, null, -1);
	}

	/**
	 * <pre>
	 * 基于 apache {@link HttpClients} 的 POST 异步（json）请求模拟
	 * </pre>
	 * @param address 请求地址
	 * @param json json
	 * @param charset 编码
	 * @return 请求反馈
	 * @throws Exception
	 */
	public static String post(String address, String json, String charset) throws Exception {
		return post(address, json, charset, null, null, null, -1);
	}

	/**
	 * <pre>
	 * 基于 apache {@link HttpClients} 的 GET 请求模拟
	 * </pre>
	 * @param address 请求地址
	 * @param charset 编码
	 * @param headerSet 请求头设置 [可空]
	 * @param proxyScheme 代理协议 [可空]
	 * @param proxyAddress 代理地址 [可空]
	 * @param proxyPort 代理端口
	 * @return 请求反馈
	 * @throws Exception
	 */
	public static String get(String address, String charset, Callable<HttpGet, Void> headerSet,
			String proxyScheme, String proxyAddress, int proxyPort) throws Exception {
		// 1. 创建 httpclient 对象
		CloseableHttpClient client = getHttpClient(proxyScheme, proxyAddress, proxyPort);
		// 2. 创建 GET 方法
		HttpGet get = new HttpGet(address);
		// 3. 设置 GET 参数
		if (ValidateCommons.isNotEmpty(headerSet)) {
			headerSet.call(get);
		}
		// 5. 执行 POST 方法，返回 HttpResponse 对象
		CloseableHttpResponse httpResponse = client.execute(get);
		int responseCode = httpResponse.getStatusLine().getStatusCode();
		HttpEntity responseEntity = httpResponse.getEntity();
		// 6. 接收响应
		String response = "";
		try {
			if (200 == responseCode) {
				// 200 服务端响应成功
				response = EntityUtils.toString(responseEntity, charset);
			} else {
				Logger.error(URLConnectionCommons.class, StringCommons.merger(
						StringCommons.NEW_LINE,
						"请求失败，响应码 [", responseCode, "]",
						StringCommons.NEW_LINE,
						EntityUtils.toString(responseEntity, charset),
						StringCommons.NEW_LINE
				));
			}
		} finally {
			if (ValidateCommons.isNotEmpty(responseEntity)) {
				StreamCommons.closePure(responseEntity.getContent());
			}
			StreamCommons.closePure(httpResponse);
		}
		return response;
	}

	/**
	 * <pre>
	 * 基于 apache {@link HttpClients} 的 GET 请求模拟
	 * </pre>
	 * @param address 请求地址
	 * @param charset 编码
	 * @param headerSet 请求头设置 [可空]
	 * @return 请求反馈
	 * @throws Exception
	 */
	public static String get(String address, String charset, Callable<HttpGet, Void> headerSet) throws Exception {
		return get(address, charset, headerSet, null, null, -1);
	}

	/**
	 * <pre>
	 * 基于 apache {@link HttpClients} 的 GET 请求模拟
	 * </pre>
	 * @param address 请求地址
	 * @param charset 编码
	 * @return 请求反馈
	 * @throws Exception
	 */
	public static String get(String address, String charset) throws Exception {
		return get(address, charset, null, null, null, -1);
	}

	/**
	 * <pre>
	 * 基于 apache {@link HttpClients} 的 POST 请求模拟
	 * </pre>
	 * @param address 请求地址
	 * @param entity http 请求实体
	 * @param charset 编码
	 * @param headerSet 请求头设置 [可空]
	 * @param proxyScheme 代理协议 [可空]
	 * @param proxyAddress 代理地址 [可空]
	 * @param proxyPort 代理端口
	 * @return 请求反馈
	 * @throws Exception
	 */
	public static String post(String address, HttpEntity entity, String charset, Callable<HttpPost, Void> headerSet,
			String proxyScheme, String proxyAddress, int proxyPort) throws Exception {
		// 1. 创建 httpclient 对象
		CloseableHttpClient client = getHttpClient(proxyScheme, proxyAddress, proxyPort);
		// 2. 创建 POST 方法
		HttpPost post = new HttpPost(address);
		// 3. 设置 POST 参数
		if (ValidateCommons.isNotEmpty(headerSet)) {
			headerSet.call(post);
		}
		// 4. 构建请求 body
		if (ValidateCommons.isNotEmpty(entity)) {
			post.setEntity(entity);
		}
		// 5. 执行 POST 方法，返回 HttpResponse 对象
		CloseableHttpResponse httpResponse = client.execute(post);
		int responseCode = httpResponse.getStatusLine().getStatusCode();
		HttpEntity responseEntity = httpResponse.getEntity();
		// 6. 接收响应
		String response = "";
		try {
			if (200 == responseCode) {
				// 200 服务端响应成功
				response = EntityUtils.toString(responseEntity, charset);
			} else {
				Logger.error(URLConnectionCommons.class, StringCommons.merger(
						StringCommons.NEW_LINE,
						"请求失败，响应码 [", responseCode, "]",
						StringCommons.NEW_LINE,
						EntityUtils.toString(responseEntity, charset),
						StringCommons.NEW_LINE
				));
			}
		} finally {
			if (ValidateCommons.isNotEmpty(responseEntity)) {
				StreamCommons.closePure(responseEntity.getContent());
			}
			StreamCommons.closePure(httpResponse);
		}
		return response;
	}

	/**
	 * <pre>
	 * 基于 apache {@link HttpClients} 的 POST 请求模拟
	 * </pre>
	 * @param address 请求地址
	 * @param entity http 请求实体
	 * @param charset 编码
	 * @param headerSet 请求头设置 [可空]
	 * @return 请求反馈
	 * @throws Exception
	 */
	public static String post(String address, HttpEntity entity, String charset, Callable<HttpPost, Void> headerSet) throws Exception {
		return post(address, entity, charset, headerSet, null, null, -1);
	}

	/**
	 * <pre>
	 * 基于 apache {@link HttpClients} 的 POST 请求模拟
	 * </pre>
	 * @param address 请求地址
	 * @param entity http 请求实体
	 * @param charset 编码
	 * @return 请求反馈
	 * @throws Exception
	 */
	public static String post(String address, HttpEntity entity, String charset) throws Exception {
		return post(address, entity, charset, null, null, null, -1);
	}

	private static CloseableHttpClient getHttpClient(String proxyScheme, String proxyAddress, int proxyPort) throws Exception {
		SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory(getSSLContext(), NoopHostnameVerifier.INSTANCE);
		Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory> create()
				.register(http, PlainConnectionSocketFactory.INSTANCE)
				.register(https, socketFactory)
				.build();
		PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry);
		Builder builder = RequestConfig.custom();
		builder
		.setCookieSpec(CookieSpecs.STANDARD_STRICT) // Cookie 策略为标准策略
		.setExpectContinueEnabled(Boolean.TRUE)
		.setTargetPreferredAuthSchemes(new ParamList<String>().addParam(AuthSchemes.NTLM).addParam(AuthSchemes.DIGEST))
		.setProxyPreferredAuthSchemes(new ParamList<String>().addParam(AuthSchemes.BASIC));
		if (ValidateCommons.isNotEmpty(proxyScheme) && ValidateCommons.isNotEmpty(proxyAddress) && proxyPort >= 0) {
			Logger.debug(URLConnectionCommons.class, StringCommons.merger("使用代理：[", proxyScheme, "] ", proxyAddress, ":", proxyPort));
			builder.setProxy(new HttpHost(proxyAddress, proxyPort, proxyScheme));
		}
		RequestConfig requestConfig = builder.build();
		return HttpClients.custom()
				.setConnectionManager(connectionManager)
				.setDefaultRequestConfig(requestConfig)
				.build();
	}

	private static SSLContext getSSLContext() throws Exception {
		// 忽略服务器端证书链的认证
		X509TrustManager trustManager = new X509TrustManager() {
			@Override
			public X509Certificate[] getAcceptedIssuers() {
				return null;
			}

			@Override
			public void checkClientTrusted(X509Certificate[] certs, String authType) {}

			@Override
			public void checkServerTrusted(X509Certificate[] certs, String authType) {}
		};
		SSLContext sslContext = SSLContext.getInstance(SSLConnectionSocketFactory.TLS);
		sslContext.init(null, new TrustManager[] {trustManager}, null);
		return sslContext;
	}

}
